package com.nulldev.util.VariableAPI;

import java.text.NumberFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;

import com.nulldev.util.JVM.Arguments;
import com.nulldev.util.VariableAPI.util.strings.splitters.Splitters;
import com.nulldev.util.data.Variables;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;
import com.nulldev.util.manage.MarkedForRemoval;

public class MathUtil {
	private static final Logger log = LoggerFactory.getLoggerD(Arguments.hasArgument("--MathUtil.debug"));

	public static class lNumberUtils {

		public static String add(String addend1, String addend2) {
			final int len_diff = addend1.length() - addend2.length();
			final int new_len = addend1.length() + makePositive(len_diff);
			final StringBuilder buf = new StringBuilder(new_len);
			for (int i1 = addend1.length() - 1, i2 = addend2.length() - 1, carry = 0; i1 >= 0 || i2 >= 0 || carry != 0; i1--, i2--) {
				int digit1 = i1 < 0 ? 0 : Integer.parseInt(Character.toString(addend1.charAt(i1)));
				int digit2 = i2 < 0 ? 0 : Integer.parseInt(Character.toString(addend2.charAt(i2)));

				int digit = digit1 + digit2 + carry;
				if (digit > 9) {
					carry = 1;
					digit -= 10;
				} else {
					carry = 0;
				}

				buf.append(digit);
			}
			return buf.reverse().toString();
		}

	}

	public static class TimeUtil {
		public static String formatSecondsL(final long timeInSecondsLong) {
			long timeInSeconds = Long.parseLong(String.valueOf(timeInSecondsLong).replace("-", ""));
			long hours = timeInSeconds / 3600;
			long secondsLeft = timeInSeconds - hours * 3600;
			long minutes = secondsLeft / 60;
			long seconds = secondsLeft - minutes * 60;
			final StringBuilder formattedTime = new StringBuilder();
			if (hours < 10)
				formattedTime.append("0");
			formattedTime.append(hours + ":");

			if (minutes < 10)
				formattedTime.append("0");
			formattedTime.append(minutes + ":");

			if (seconds < 10)
				formattedTime.append("0");
			formattedTime.append(seconds);

			return formattedTime.toString();
		}

		public static long fromHours(final int hours) {
			return hours * 60 * 60 * 1000;
		}

		public static long fromMinutes(final int minutes) {
			return minutes * 60 * 1000;
		}

		public static long fromSeconds(final int seconds) {
			return seconds * 1000;
		}

		/**
		 * Convert time in milliseconds to the corresponding string, in case of day
		 * Rollover start from scratch 23:59:59.999 + 1 = 00:00:00.000
		 *
		 * @param millis the number of milliseconds corresponding to tim i.e. 34137999
		 *               that can be obtained as follows;
		 *               <p>
		 *               long lH = h * 60 * 60 * 1000; //hour to milli
		 *               <p>
		 *               long lM = m * 60 * 1000; // minute to milli
		 *               <p>
		 *               long lS = s * 1000; //seconds to milli
		 *               <p>
		 *               millis = lH + lM + lS + ms;
		 * @return a string formatted as HH:MM:SS.MSMS i.e. "23:59:59.999"
		 */
		public static String milliToString(final long millis) {
			return milliToString(millis, false);
		}

		/**
		 * Convert time in milliseconds to the corresponding string, in case of day
		 * Rollover start from scratch 23:59:59.999 + 1 = 00:00:00.000
		 *
		 * @param millis the number of milliseconds corresponding to tim i.e. 34137999
		 *               that can be obtained as follows;
		 *               <p>
		 *               long lH = h * 60 * 60 * 1000; //hour to milli
		 *               <p>
		 *               long lM = m * 60 * 1000; // minute to milli
		 *               <p>
		 *               long lS = s * 1000; //seconds to milli
		 *               <p>
		 *               millis = lH + lM + lS + ms;
		 * @return a string formatted as HH:MM:SS.MSMS i.e. "23:59:59.999"
		 */
		public static String milliToString(final long millis, final boolean shorterMLSWorkaround) {
			long hrs = TimeUnit.MILLISECONDS.toHours(millis) % 24;
			long min = TimeUnit.MILLISECONDS.toMinutes(millis) % 60;
			long sec = TimeUnit.MILLISECONDS.toSeconds(millis) % 60;
			long mls = millis % 1000;
			if (shorterMLSWorkaround) {
				/* For some reason %02d doesn't want to work on the final value, cool. */
				String uglyWorkaround = Long.toString(mls);
				if (uglyWorkaround.length() > 2)
					uglyWorkaround = uglyWorkaround.substring(0, 2);
				return String.format("%02d:%02d:%02d.", hrs, min, sec).concat(uglyWorkaround);
			} else {
				return String.format("%02d:%02d:%02d.%03d", hrs, min, sec, mls);
			}
		}

		/**
		 * Convert time in milliseconds to the corresponding string, in case of day
		 * Rollover start from scratch 23:59:59.999 + 1 = 00:00:00.000
		 *
		 * @param millis the number of milliseconds corresponding to tim i.e. 34137999
		 *               that can be obtained as follows;
		 *               <p>
		 *               long lH = h * 60 * 60 * 1000; //hour to milli
		 *               <p>
		 *               long lM = m * 60 * 1000; // minute to milli
		 *               <p>
		 *               long lS = s * 1000; //seconds to milli
		 *               <p>
		 *               millis = lH + lM + lS + ms;
		 * @return a string formatted as HH:MM:SS.MSMS i.e. "23:59:59.999"
		 */
		public static String milliToStringSRT(final long millis) {
			long hrs = TimeUnit.MILLISECONDS.toHours(millis) % 24;
			long min = TimeUnit.MILLISECONDS.toMinutes(millis) % 60;
			long sec = TimeUnit.MILLISECONDS.toSeconds(millis) % 60;
			long mls = millis % 1000;
			return String.format("%02d:%02d:%02d,%03d", hrs, min, sec, mls);
		}

		public final static long MOV_TIME_OFFSET = __getMovTimeOffset();

		private static long __getMovTimeOffset() {
			try {
				final Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
				calendar.set(1904, 0, 1, 0, 0, 0);
				calendar.set(Calendar.MILLISECOND, 0);
				return calendar.getTimeInMillis();
			} catch (Exception e) {
				log.error("Failed to calculate MOV_TIME_OFFSET!", e);
				return -1L;
			}
		}

		public static Date macTimeToDate(int movSec) {
			return new Date(fromMovTime(movSec));
		}

		public static long fromMovTime(int movSec) {
			return ((long) movSec) * 1000L + MOV_TIME_OFFSET;
		}

		public static int toMovTime(long millis) {
			return (int) ((millis - MOV_TIME_OFFSET) / 1000L);
		}
	}

	public static class TimeToSeconds {
		public static long parseTime(final String str) throws NumberFormatException {
			if (str == null)
				throw new NullPointerException("parseTimeString() --> null str");
			if (str.isEmpty())
				throw new NullPointerException("parseTimeString() --> empty str");
			if (str.startsWith("-")) {
				return MathUtil.safeParseL(str, -1);
			}
			int h = 0;
			int m, s;
			final String units[] = Splitters.splitString(str, ":");
			if (units.length != 2 && units.length != 3)
				throw new RuntimeException("unknown unit count: " + units.length + " (input: " + str + ")");
			switch (units.length) {
				case 2:
					// mm:ss
					m = Integer.parseInt(units[0]);
					s = Integer.parseInt(units[1]);
					break;
				case 3:
					// hh:mm:ss
					h = Integer.parseInt(units[0]);
					m = Integer.parseInt(units[1]);
					s = Integer.parseInt(units[2]);
					break;
				default:
					throw new NumberFormatException("parseTimeString() --> failed:" + str);
			}
			if (m < 0 || m > 60 || s < 0 || s > 60 || h < 0) {
				throw new NumberFormatException("parseTimeString() --> range error:" + str);
			}
			return h * 3600 + m * 60 + s;
		}

		public static long parseTimeStringToSeconds(final String str) {
			try {
				return parseTime(str);
			} catch (NumberFormatException nfe) {
				return 0;
			}
		}

	}

	public static long getLong(byte[] array, int offset) {
		return ((long) (array[offset] & 0xff) << 56) | ((long) (array[offset + 1] & 0xff) << 48) | ((long) (array[offset + 2] & 0xff) << 40)
				| ((long) (array[offset + 3] & 0xff) << 32) | ((long) (array[offset + 4] & 0xff) << 24) | ((long) (array[offset + 5] & 0xff) << 16)
				| ((long) (array[offset + 6] & 0xff) << 8) | ((long) (array[offset + 7] & 0xff));
	}

	public static int getInt(byte[] array, int offset) {
		return ((array[offset] & 0xff) << 24) | ((array[offset + 1] & 0xff) << 16) | ((array[offset + 2] & 0xff) << 8) | (array[offset + 3] & 0xff);
	}

	public static int safeLongToInt(long l) {
		if (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE) {
			throw new IllegalArgumentException(l + " cannot be cast to int without changing its value.");
		}
		return (int) l;
	}

	public static int safeParseInt(final String text, final int fallback) {
		if (text == null) {
			return fallback;
		}
		try {
			return Integer.parseInt(text);
		} catch (Exception e) {
			return fallback;
		}
	}

	public static short safeParseShort(final String text, final short fallback) {
		if (text == null) {
			return fallback;
		}
		try {
			return Short.parseShort(text);
		} catch (Exception e) {
			return fallback;
		}
	}

	public static double safeParseDouble(final String text, final double fallback) {
		try {
			return Double.parseDouble(text);
		} catch (Exception e) {
			return fallback;
		}
	}

	public static double safeParseDoubleWithCommas(final String text, final double fallback) {
		try {
			return Double.parseDouble(text.replace(',', '.'));
		} catch (Exception e) {
			return fallback;
		}
	}

	public static double safeParseLargeDoubleWithCommas(final String text, final double fallback) {
		try {
			return Double.parseDouble(StringsUtil.replace(text, ",", ""));
		} catch (Exception e) {
			return fallback;
		}
	}

	public static double safeParseLargeDoubleWithCommasA(final String text, final double fallback) {
		try {
			return Double.parseDouble(StringsUtil.replace(text, ",", "."));
		} catch (Exception e) {
			return fallback;
		}
	}

	public static long safeParseL(final String text, final long fallback) {
		try {
			return Long.parseLong(text);
		} catch (Exception e) {
			return fallback;
		}
	}

	public static float safeParseF(final String text, final float fallback) {
		try {
			return Float.parseFloat(text);
		} catch (Exception e) {
			return fallback;
		}
	}

	public static int safeParseList(final String[] data, final int index, final int fallback) {
		try {
			return Integer.parseInt(data[index]);
		} catch (Exception e) {
			return fallback;
		}
	}

	public static String toHex(final int num) {
		return Integer.toHexString(num);
	}

	public static String intToHexStr(final int i) {
		return "0x" + String.format("%2s", Integer.toHexString(i)).replace(' ', '0');
	}

	public static String toHex(final long num) {
		return Long.toHexString(num);
	}

	public static float clamp(final float min, final float max, final float val) {
		if (val < min)
			return min;
		else if (val > max)
			return max;
		else
			return val;
	}

	public static long clamp(final long min, final long max, final long val) {
		if (val < min)
			return min;
		else if (val > max)
			return max;
		else
			return val;
	}

	public static double clamp(final double min, final double max, final double val) {
		if (val < min)
			return min;
		else if (val > max)
			return max;
		else
			return val;
	}

	public static int clamp(final int min, final int max, final int val) {
		if (val < min)
			return min;
		else if (val > max)
			return max;
		else
			return val;
	}

	public static void putLong(long value, byte[] array, int offset) {
		array[offset] = (byte) (0xff & (value >> 56));
		array[offset + 1] = (byte) (0xff & (value >> 48));
		array[offset + 2] = (byte) (0xff & (value >> 40));
		array[offset + 3] = (byte) (0xff & (value >> 32));
		array[offset + 4] = (byte) (0xff & (value >> 24));
		array[offset + 5] = (byte) (0xff & (value >> 16));
		array[offset + 6] = (byte) (0xff & (value >> 8));
		array[offset + 7] = (byte) (0xff & value);
	}

	public static void putInt(int value, byte[] array, int offset) {
		array[offset] = (byte) (0xff & (value >> 24));
		array[offset + 1] = (byte) (0xff & (value >> 16));
		array[offset + 2] = (byte) (0xff & (value >> 8));
		array[offset + 3] = (byte) (0xff & value);
	}

	@Deprecated
	@MarkedForRemoval(reason = "Replaced by MathUtilFormatter.", plannedRemovalWhen = "3.0.3")
	public static long fromFormat(final String _org_text) {
		log.warn("format(String) will be removed soon, you should use MathUtilFormatter instead.");
		log.debug("fromFormat.text -> " + _org_text);
		String text = _org_text;
		text = Splitters.splitString(_org_text, " ")[0];
		boolean USE_NEW_METHOD = false;
		if (_org_text.contains(" mln ") || _org_text.contains(" tys. ") || _org_text.contains(" mld ")) {
			log.debug("fromFormat.text -> HAS_PL_NUMS=TRUE");
			text += " " + Splitters.splitString(_org_text, " ")[1] + " ";
			USE_NEW_METHOD = true;
		}
		long factor = 1;
		final boolean expanded = _org_text.endsWith("B") || _org_text.endsWith("M") || _org_text.endsWith("K") || _org_text.contains("mln")
				|| _org_text.contains("tys.") || _org_text.contains("mld");
		log.debug("fromFormat.text -> EXPANDED=" + expanded);
		if (_org_text.endsWith("B") || _org_text.contains("mld")) {
			factor = (long) 1e9;
		} else if (_org_text.endsWith("M") || _org_text.contains("mln")) {
			factor = (long) 1e6;
		} else if (_org_text.endsWith("K") || _org_text.contains("tys.")) {
			factor = (long) 1e3;
		}
		text = Splitters.splitString(_org_text, " ")[0];
		log.debug("fromFormat.text -> text=" + text);
		final String toS = (USE_NEW_METHOD ? text : text.substring(0, text.length() - 1));
		log.debug("fromFormat.text -> toS=" + toS);
		String x0 = (expanded && !USE_NEW_METHOD ? toS : text).replaceAll(",", "");
		log.debug("fromFormat.text -> x0=" + x0);
		log.debug("fromFormat.factor -> " + factor);
		double l = MathUtil.safeParseDouble(x0, -1);
		if (l != -1) {
			l *= factor;
		}
		return (long) l;
	}

	private static NumberFormat format;
	static {
		if (format == null) {
			format = NumberFormat.getInstance();
			format.setGroupingUsed(true);
		}
	}

	public static String formatNumber(final Number number) {
		return format.format(number);
	}

	public static boolean stringBetweenList(final String[] data, final int index, final int min, final int max) {
		try {
			return Integer.parseInt(data[index]) > min && Integer.parseInt(data[index]) < max;
		} catch (Exception e) {
			return false;
		}
	}

	public static boolean stringBetweenListRC(final String[] data, final int index, final int min, final int max) {
		try {
			return Integer.parseInt(data[index].replaceAll("[^\\d]", "")) > min && Integer.parseInt(data[index].replaceAll("[^\\d]", "")) < max;
		} catch (Exception e) {
			return false;
		}
	}

	public static int[] swapFirstAndSecondHalf(int[] values) {
		boolean evenSize = values.length % 2 == 0;
		int half = values.length / 2;
		int[] swapper = new int[values.length];
		System.arraycopy(values, evenSize ? half : half + 1, swapper, 0, half);
		System.arraycopy(values, 0, swapper, evenSize ? half : half + 1, half);
		if (!evenSize) {
			swapper[half] = values[half];
		}
		return swapper;
	}

	public static long[] swapFirstAndSecondHalf(long[] values) {
		boolean evenSize = values.length % 2 == 0;
		int half = values.length / 2;
		long[] swapper = new long[values.length];
		System.arraycopy(values, evenSize ? half : half + 1, swapper, 0, half);
		System.arraycopy(values, 0, swapper, evenSize ? half : half + 1, half);
		if (!evenSize) {
			swapper[half] = values[half];
		}
		return swapper;
	}

	public static short autoConvertToShort(final Object _queryItem) {
		if (!(_queryItem instanceof Number))
			return 0;
		if (_queryItem instanceof Short) {
			return (Short) _queryItem;
		} else if (_queryItem instanceof Integer) {
			int i = (int) _queryItem;
			short s;
			if (i >= Short.MAX_VALUE)
				s = Short.MAX_VALUE;
			else if (i <= Short.MIN_VALUE)
				s = Short.MIN_VALUE;
			else
				s = (short) i;
			return s;
		} else if (_queryItem instanceof Long) {
			long i = (long) _queryItem;
			short s;
			if (i >= Short.MAX_VALUE)
				s = Short.MAX_VALUE;
			else if (i <= Short.MIN_VALUE)
				s = Short.MIN_VALUE;
			else
				s = (short) i;
			return s;
		} else
			return (short) _queryItem;
	}

	public static int roundUp(final int num, final int multipleOf) {
		return (int) (Math.ceil(num / (double) multipleOf) * multipleOf);
	}

	public static long autoConvertToLong(final Object _queryItem) {
		if (_queryItem instanceof String)
			return autoConvertToInt(StringsUtil.asNumber((String) _queryItem));
		if (!(_queryItem instanceof Number))
			return 0;
		if (_queryItem instanceof Short)
			return (Short) _queryItem;
		if (_queryItem instanceof Integer)
			return (long) (int) _queryItem;
		if (_queryItem instanceof Double)
			return (long) (double) _queryItem;
		return (long) _queryItem;
	}

	public static int autoConvertToInt(final Object _queryItem) {
		if (_queryItem instanceof String)
			return autoConvertToInt(StringsUtil.asNumber((String) _queryItem));
		if (!(_queryItem instanceof Number))
			return 0;
		if (_queryItem instanceof Short)
			return (Short) _queryItem;
		if (_queryItem instanceof Integer)
			return (int) _queryItem;
		if (_queryItem instanceof Double)
			return (int) (double) _queryItem;
		if (_queryItem instanceof Long) {
			long i = (long) _queryItem;
			int s;
			if (i >= Integer.MAX_VALUE)
				s = Integer.MAX_VALUE;
			else if (i <= Integer.MIN_VALUE)
				s = Integer.MIN_VALUE;
			else
				s = (int) i;
			return s;
		}
		return (int) _queryItem;
	}

	public static int makePositive(final int num) {
		if (num >= 0)
			return num;
		return -num;
	}

	public static long makePositive(final long num) {
		if (num >= 0)
			return num;
		return -num;
	}

	public static void checkPositiveOrZero(final Number number, final String name) {
		if (Variables.requireNonNullObject(number, "number").intValue() <= 0)
			throw new IllegalArgumentException(name + " is 0 or negative!");
	}

	public static int padToInterval(final int i, final int interval) {
		if (i % interval != 0) {
			if (i < interval)
				return i + (interval - i);
			final int diff = (i + interval) % interval;
			return i + (interval - diff);
		} else {
			return i;
		}
	}

	/* Start GUAVA code */

	static void checkNoOverflow(boolean condition, String methodName, int a, int b) {
		if (!condition) {
			throw new ArithmeticException("overflow: " + methodName + "(" + a + ", " + b + ")");
		}
	}

	static void checkNoOverflow(boolean condition, String methodName, long a, long b) {
		if (!condition) {
			throw new ArithmeticException("overflow: " + methodName + "(" + a + ", " + b + ")");
		}
	}

	/**
	 * Returns the product of {@code a} and {@code b}, provided it does not
	 * overflow.
	 *
	 * @throws ArithmeticException if {@code a * b} overflows in signed {@code int}
	 *                             arithmetic
	 */
	public static int checkedMultiply(int a, int b) {
		long result = (long) a * b;
		checkNoOverflow(result == (int) result, "checkedMultiply", a, b);
		return (int) result;
	}

	/* END GUAVA CODE */

	public static double round(double value, final int places) {
		if (places < 0)
			throw new IllegalArgumentException("places < 0");

		long factor = (long) Math.pow(10, places);
		value = value * factor;
		long tmp = Math.round(value);
		return (double) tmp / factor;
	}
}
